
package todolist.logic.task;

import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.ListIterator;
import todolist.entity.*;
import todolist.logic.parser.*;
import todolist.logic.reminder.ReminderExpiredListener;
import todolist.logic.reminder.ReminderTaskChangedListener;
import todolist.logic.task.comparator.*;
import todolist.personalization.DateTimeDescriptionCustomizer;
import todolist.personalization.ErrorMessages;
import todolist.personalization.PriorityCustomizer;
import todolist.storage.TaskImportedListener;
import todolist.util.DateUtils;
import todolist.util.FieldFormatter;
import todolist.util.Utils;

/**
 * Controller facade for Task core subsystem, Singleton class.
 * @author You Li
 * @author Lina
 */
public class TaskController implements ReminderExpiredListener, TaskImportedListener {
	
	// handle singleton instance

	private static TaskController _objTC;
	
	public static TaskController getInstance() {
		if (_objTC == null)
			_objTC = new TaskController();
		return _objTC;
	}
	
	// member attributes

	private TaskContainerEntity _entTCE;
	private String _strHeader;
	private StringBuffer _objSB;
	
	private ArrayList<TaskChangedListener> _objTCList; // listener
	private ArrayList<ReminderTaskChangedListener> _objRTCList; // listener
	
	private static final String HEADER_SPACER = "|";
	
	private TaskController() {
		_entTCE = new TaskContainerEntity();
		setHeader(null, null);
		_objSB = new StringBuffer();
		_objTCList = new ArrayList<TaskChangedListener>();
		_objRTCList = new ArrayList<ReminderTaskChangedListener>();
	}
	
	// listener methods
	
	public void addTaskChangedListener(TaskChangedListener pobjTCListener) {
		_objTCList.add(pobjTCListener);
	}
	
	public boolean removeTaskChangedListener(TaskChangedListener pobjTCListener) {
		return _objTCList.remove(pobjTCListener);
	}
	
	public void addReminderTaskChangedListener(ReminderTaskChangedListener pobjRTCListener) {
		_objRTCList.add(pobjRTCListener);
	}
	
	public boolean removeReminderTaskChangedListener(ReminderTaskChangedListener pobjRTCListener) {
		return _objRTCList.remove(pobjRTCListener);
	}

	// business facilitator methods
	
	/**
	 * Adds a Task and returns the newly created/added Task. O(1).
	 * Assumes all input fields have been validated.
	 * @throws IllegalArgumentException
	 */
	public void addTask(
		String pstrName, String pstrDT, PriorityType penmPriority, int pintReminderOffset, String pstrComments)
		throws IllegalArgumentException {

		// parse DT
		DateTimeParser objDTParser = new DateTimeParser();
		DateTimeParseResult objParseResult = objDTParser.parseDTDescription(pstrDT);
		if (objParseResult.getError() != null)
			throw new IllegalArgumentException("TaskController.addTask(): Failed to parse Date/Time.");
		DateTimeEntity entStartDT = objParseResult.getStartDT();
		DateTimeEntity entEndDT = objParseResult.getEndDT();
		
		// create Task Entity
		long lngTaskID = _entTCE.getLastTaskId() + 1;
		TaskEntity entNewTask = new TaskEntity(lngTaskID, pstrName);
		entNewTask.setStartDT(entStartDT);
		entNewTask.setEndDT(entEndDT);
		if (penmPriority != null)
			entNewTask.setPriority(penmPriority);
		entNewTask.setTaskComments(pstrComments);
		entNewTask.setReminderOffset(pintReminderOffset);
		
		// add Task Entity
		_entTCE.addTask(entNewTask);

		// add short details to front of list all buffer
		setHeader(null, null);
		StringBuffer objTempSB = new StringBuffer();
		objTempSB.append(entNewTask.toShortString() + Utils.getLineSeparator());
		objTempSB.append(_objSB);
		_objSB = objTempSB;

		// notify listeners
		for (TaskChangedListener objTCListener : _objTCList)
			objTCListener.taskPostAdd((TaskEntity)entNewTask.clone());
		for (ReminderTaskChangedListener objRTCListener : _objRTCList)
			objRTCListener.reminderTaskMayHaveChanged((TaskEntity) entNewTask.clone()); // new task will never be null
	}

	/**
	 * Edits a Task. O(n).
	 * Assumes all input fields have been validated.
	 * @throws IllegalArgumentException
	 */
	public void editTask(long plngTaskId, TaskFieldType penmField, String pstrValue) throws IllegalArgumentException {
		TaskEntity entEditTask = _entTCE.findTask(plngTaskId);
		if (entEditTask == null)
			throw new IllegalArgumentException("TaskController.editTask(): Task with given ID not found.");
		
		TaskEntity entOldTaskState = (TaskEntity) entEditTask.clone(); // preserve old state for notifying listeners later
		
		switch(penmField) {
			case TASK_FIELD_TASKNAME:
				entEditTask.setTaskName(pstrValue);
				break;
			case TASK_FIELD_DT:
				DateTimeParser objDTParser = new DateTimeParser();
				DateTimeParseResult objDTPResult = objDTParser.parseDTDescription(pstrValue);
				if (objDTPResult.getError() != null)
					throw new IllegalArgumentException("TaskController.editTask(): Failed to parse Date/Time.");
				entEditTask.setBothDT(objDTPResult.getStartDT(), objDTPResult.getEndDT());
				break;
			case TASK_FIELD_PRIORITY:
				PriorityParser objPP = new PriorityParser();
				PriorityType enmPriority = objPP.parsePriority(pstrValue);
				if (enmPriority == null)
					enmPriority = PriorityCustomizer.getInstance().getDefaultPriorityType();
				entEditTask.setPriority(enmPriority);
				break;
			case TASK_FIELD_COMMENTS:
				entEditTask.setTaskComments(pstrValue);
				break;
			case TASK_FIELD_REMINDER:
				ReminderOffsetParser objROP = new ReminderOffsetParser();
				ReminderOffsetParseResult objROPResult = objROP.parseReminderOffset(pstrValue);
				if (objROPResult.getError() != null)
					throw new IllegalArgumentException("TaskController.editTask(): Failed to parse Reminder Offset.");
				entEditTask.setReminderOffset(objROPResult.getReminderOffset());
				break;
			default: // shouldn't happen, Field Type null or set Status
				throw new IllegalArgumentException("TaskController.editTask(): Invalid Task Field Type.");
		}

		setHeader(null, null);
		updateInternalListAll();
		
		// notify listeners
		for (TaskChangedListener objTCListener : _objTCList)
			objTCListener.taskPostEdit(entOldTaskState, (TaskEntity)entEditTask.clone());
		setReminder();
	}
	
	/**
	 * Deletes a Task. O(n).
	 * @throws IllegalArgumentException
	 */
	public void deleteTask(long plngTaskId) throws IllegalArgumentException {
		TaskEntity entTaskFound = _entTCE.findTask(plngTaskId);
		if (entTaskFound == null)
			throw new IllegalArgumentException("TaskController.deleteTask(): Task with given ID not found.");
		
		_entTCE.deleteTask(plngTaskId);
		setHeader(null, null);
		updateInternalListAll();
		
		// notify listeners
		for (TaskChangedListener objTCListener : _objTCList)
			objTCListener.taskPostDelete((TaskEntity) entTaskFound); // no need to clone, Task already deleted	
		setReminder();
	}
	
	/**
	 * Lists short status of each Task, for display. O(1).
	 * If there are no Tasks, empty String returned.
	 */
	public String getListAllString() {
		return
			_strHeader + Utils.getLineSeparator() + Utils.getLineSeparator() +
			_objSB.toString();
	}
	
	/**
	 * Returns full details of Task by Task ID, or empty String if Task not found.
	 * Assumes Task ID represents an existing Task.
	 * @throws IllegalArgumentException
	 */
	public String getShowDetailsString(long plngTaskId) throws IllegalArgumentException {
		TaskEntity entTask = _entTCE.findTask(plngTaskId);
		if (entTask == null)
			throw new IllegalArgumentException("TaskController.getShowDetailsString(): Task with given ID not found.");
		return entTask.toString();
	}

	/**
	 * Returns full details of Tasks matched by name/comments, or empty String if no results. O(Qn).
	 * @throws IllegalArgumentException
	 */
	public String getSearchResultsString(String pstrQuery) throws IllegalArgumentException {
		if (pstrQuery == null)
			throw new IllegalArgumentException("TaskController.getSearchResultsString(): Search query is mandatory.");
		String[] arrQuery = pstrQuery.split("\\s+");
		ListIterator <TaskEntity> objItr = _entTCE.getListIterator();
		String strResult = "";
		while (objItr.hasNext()) {
			TaskEntity entTask = objItr.next();
			for (String strCurrQuery : arrQuery) { // O(Q) each
				if (entTask.getTaskName().toUpperCase().contains(strCurrQuery.toUpperCase()) ||
					entTask.getTaskComments().toUpperCase().contains(strCurrQuery.toUpperCase())) {
					strResult += entTask.toString() + Utils.getLineSeparator();
					break; // this task added, move on to next task; yes, break, not continue
				}
			}
		}
		return strResult;
	}
	
	/**
	 * Sorts Task Container by given Property and Direction. O(n log n).
	 * @throws IllegalArgumentException
	 */
	public void sort(TaskFieldType penmField, SortDirectionType penmSortDirection) throws IllegalArgumentException {
		if (penmSortDirection == null || penmField == null)
			throw new IllegalArgumentException("TaskController.sort(): Sort requires Direction and Type.");
		if (penmSortDirection == SortDirectionType.SORT_DESC)
			switch (penmField) {
				case TASK_FIELD_TASKNAME:
					_entTCE.sortTasks(new TaskNameDescending()); break;
				case TASK_FIELD_DT:
					_entTCE.sortTasks(new EndDateDescending()); break;
				case TASK_FIELD_PRIORITY:
					_entTCE.sortTasks(new PriorityDescending()); break;
				case TASK_FIELD_STATUS:
					_entTCE.sortTasks(new TaskStatusDescending()); break;
				default: // shouldn't happen =X
					throw new IllegalArgumentException("TaskController.sort(): Invalid sort Type.");
			}
		else if (penmSortDirection == SortDirectionType.SORT_ASC)
			switch (penmField) {
				case TASK_FIELD_TASKNAME:
					_entTCE.sortTasks(new TaskNameAscending()); break;
				case TASK_FIELD_DT:
					_entTCE.sortTasks(new EndDateAscending()); break;
				case TASK_FIELD_PRIORITY:
					_entTCE.sortTasks(new PriorityAscending()); break;
				case TASK_FIELD_STATUS:
					_entTCE.sortTasks(new TaskStatusAscending()); break;
				default: // shouldn't happen =X
					throw new IllegalArgumentException("TaskController.sort(): Invalid sort Type.");
			}
		else // shouldn't happen =X
			throw new IllegalArgumentException("TaskController.sort(): Invalid sort Direction.");

		setHeader(penmField, penmSortDirection);
		updateInternalListAll(); // + O(n)
	}
	
	/**
	 * Returns full details of all Tasks due today, empty String if no results. O(n).
	 */
	public String getTodaysTasksString(){
		Date dtmToday = DateUtils.removeTimeFromDate(new Date());
		StringBuffer objSB = new StringBuffer();
		ListIterator<TaskEntity> objItr = _entTCE.getListIterator();
		while(objItr.hasNext()) {
			TaskEntity entTask = objItr.next();
			Date dtmEnd = null;
			if (entTask.getEndDT() != null)
				dtmEnd = DateUtils.removeTimeFromDate(entTask.getEndDT().getInternalDate());
			if (dtmToday.equals(dtmEnd)) {
				objSB.append(entTask.toString());
				objSB.append(Utils.getLineSeparator());
			}
		}
		return objSB.toString();
	}
	
	// business validate methods

	/**
	 * Returns null if Task ID is valid, or Error Message otherwise.
	 */
	public String validateExistingTaskID(String pstrTaskID){
		if (pstrTaskID == null || pstrTaskID.trim().equals(""))
			return ErrorMessages.ERR_INVALID_TASK_ID;
		long lngTaskID;
		try {
			lngTaskID = Long.valueOf(pstrTaskID.trim());
		} catch (NumberFormatException objEx) {
			return ErrorMessages.ERR_INVALID_TASK_ID;
		}
		if (lngTaskID <= 0 || _entTCE.findTask(lngTaskID) == null)
			return ErrorMessages.ERR_INVALID_TASK_ID;
		return null;
	}

	/**
	 * Returns null if Task Name is valid, or Error Message otherwise.
	 */
	public String validateTaskName(String pstrTaskName){
		if(pstrTaskName == null || pstrTaskName.trim().equals("") ||
			pstrTaskName.contains(Utils.getLineSeparator()))
			return ErrorMessages.ERR_INVALID_TASK_NAME;
		return null;
	}
	
	/**
	 * Returns null if Date is valid, or Error Message otherwise.
	 */
	public String validateDT(String pstrDT) {
		DateTimeParser objDTParser = new DateTimeParser();
		DateTimeParseResult objParseResult = objDTParser.parseDTDescription(pstrDT);
		return objParseResult.getError();
	}

	/**
	 * Returns null if Priority is valid or not specified, or Error Message otherwise.
	 */
	public String validatePriority(String pstrPriority) {
		if(pstrPriority == null || pstrPriority.trim().equals(""))
			return null; // optional field, if not specified
		PriorityParser objPP = new PriorityParser();
		boolean blnPriorityNotFound = (objPP.parsePriority(pstrPriority) == null);
		if (blnPriorityNotFound)
			return ErrorMessages.ERR_INVALID_PRIORITY;
		return null;
	}

	/**
	 * Returns null if Task Comments is valid, or Error Message otherwise.
	 */
	public String validateTaskComments(String pstrComments){
		if(pstrComments == null || pstrComments.contains(Utils.getLineSeparator()))
			return ErrorMessages.ERR_INVALID_COMMENTS;
		return null;
	}

	/**
	 * Returns null if Reminder Offset is valid, or Error Message otherwise.
	 */
	public String validateReminder(String pstrOffset) {
		ReminderOffsetParser objROP = new ReminderOffsetParser();
		ReminderOffsetParseResult objParseResult = objROP.parseReminderOffset(pstrOffset);
		return objParseResult.getError();
	}

	// TODO : v0.x - recurring events
	
	/*
	public boolean validateRecurring(String pstrRecurring) {
		if (pstrRecurring.equalsIgnoreCase("Yes") ||
			pstrRecurring.equalsIgnoreCase("No"))
			return true;
		return false;
	}
	
	public boolean validateRFrequency(String rFrequency){
		if (rFrequency.equalsIgnoreCase("Daily")
				|| rFrequency.equalsIgnoreCase("Weekly")
				|| rFrequency.equalsIgnoreCase("Bi-weekly")
				|| rFrequency.equalsIgnoreCase("Monthly")
				|| rFrequency.equalsIgnoreCase("Annually"))
			return true;
		return false;
	}*/
	
	// private methods

	/**
	 * Updates List All buffer with short status of each Task in Container, for display. O(n).
	 */
	private void updateInternalListAll() {
		_objSB.setLength(0); // clear list all buffer
		ListIterator<TaskEntity> objItr = _entTCE.getListIterator();
		while(objItr.hasNext()) {
			TaskEntity entTask = objItr.next();
			_objSB.append(entTask.toShortString() + Utils.getLineSeparator());
		}
	}
	
	/**
	 * Sets header based on Sort Criteria and Direction. O(1).
	 */
	private void setHeader(TaskFieldType penmField, SortDirectionType penmDir) {
		String strDirection = "";
		if (penmDir == SortDirectionType.SORT_ASC)
			strDirection = " ^>";
		else if (penmDir == SortDirectionType.SORT_DESC)
			strDirection = " v>";
		String strTaskID = "ID";
		String strPriority = "Pri.";
		String strTaskName = "Name";
		String strEndDT = DateTimeDescriptionCustomizer.getInstance().getDTDisplayOneEndDate();
		strEndDT = String.valueOf(strEndDT.charAt(0)).toUpperCase() + strEndDT.substring(1); // capitalize 1st letter
		if (penmField == TaskFieldType.TASK_FIELD_TASKNAME)
			strTaskName = "<" + strTaskName + strDirection;
		else if (penmField == TaskFieldType.TASK_FIELD_DT)
			strEndDT = "<" + strEndDT + strDirection;
		else if (penmField == TaskFieldType.TASK_FIELD_PRIORITY)
			strPriority = "<" + strPriority + strDirection;
		else if (penmField == TaskFieldType.TASK_FIELD_TASKNAME)
			; // TODO : v0.x - Task Status
		else if (penmField == null)
			; // do nothing
		else // shouldn't happen =X
			throw new IllegalArgumentException("TaskController.setHeader(): Invalid sort Type.");
		FieldFormatter objFormatter = new FieldFormatter();
		objFormatter.addField(strTaskID, 10);
		objFormatter.addField(HEADER_SPACER, 1);
		objFormatter.addField(strPriority, 10);
		objFormatter.addField(HEADER_SPACER, 1);
		objFormatter.addField(strTaskName, 40);
		objFormatter.addField(HEADER_SPACER, 1);
		objFormatter.addField(strEndDT, 1);
		_strHeader = objFormatter.toString();
	}
	
	/**
	 * Finds earliest Reminder to current Date/Time and set it. O(n).
	 */
	private void setReminder(){
		Date dtmNow = DateUtils.removeSecondsFromDate(new Date());
		ListIterator<TaskEntity> objItr = _entTCE.getListIterator();
		GregorianCalendar objGCEarliest = new GregorianCalendar();
		objGCEarliest.setLenient(false);
		objGCEarliest.setTime(dtmNow);
		objGCEarliest.add(GregorianCalendar.YEAR, 100);
		Date dtmEarliest = objGCEarliest.getTime(); // start 100 years later, look for earlier Task
		TaskEntity entRemindingTask = null; // earliest task after current date/time
		while(objItr.hasNext()) {
			TaskEntity entTask = objItr.next();
			Date dtmCurrTask = entTask.getReminderDate();
			if (dtmCurrTask == null)
				continue; // if non-temporal event
			if (dtmCurrTask.before(dtmEarliest) && dtmCurrTask.after(dtmNow)) {
				dtmEarliest = dtmCurrTask;
				entRemindingTask = entTask;
			}
		}
		// notify listeners
		for (ReminderTaskChangedListener objRTCListener : _objRTCList)
			if (entRemindingTask == null)
				objRTCListener.reminderTaskForceChanged(null);
			else
				objRTCListener.reminderTaskForceChanged((TaskEntity) entRemindingTask.clone());
	}

	// listener implementation methods

	/**
	 * Find next Task to be schedeuled after current one expires. O(n).
	 */
	@Override
	public void reminderExpired() {
		setReminder();
	}

	/**
	 * Adds Task to Task container each time event is fired.
	 */
	@Override
	public void importTaskFromFile(String pstrName, String pstrDT, PriorityType penmPriority, int pintReminderOffset, String pstrComments) {
		addTask(pstrName, pstrDT, penmPriority, pintReminderOffset, pstrComments);
	}

}
